深入探索Jetpack Compose LazyColumn性能表现及优化策略 您所在的位置:网站首页 jetpack compose动画 深入探索Jetpack Compose LazyColumn性能表现及优化策略

深入探索Jetpack Compose LazyColumn性能表现及优化策略

2024-05-23 02:14| 来源: 网络整理| 查看: 265

自从Jetpack Compose作为Google推出的现代化UI工具包以来,它在Android开发领域掀起了不小的浪潮。Compose的声明式、响应式让UI构建变得前所未有的简洁高效。

然而,在大量数据场景下,Compose内置列表组件LazyColumn的性能却一直受到质疑和探讨。本博客将深入分析LazyColumn的工作原理,揭示它遇到的性能瓶颈,并介绍一些优化手段,最后与经典的RecyclerView进行性能对比。

LazyColumn工作原理:LazyColumn是Compose中用于呈现大量重复数据的列表组件,其设计理念借鉴了Bruce法则:数据驱动 虚拟化、离屏回收等技术。具体来说,LazyColumn会根据用户可视区域,只渲染极少部分实际可见的内容。当滚动时,Compose会智能回收离开屏幕的已渲染组合项,并重新组合新出现的项目,从而大幅节省内存和CPU开销。

然而,这种基于状态跟踪的响应式重组机制,在处理大规模列表时仍可能遇到一些瓶颈和性能问题。

性能问题分析,通过Traceview等诊断工具,我们可以详细分析LazyColumn在大数据场景下的性能表现。主要发现了以下几个方面的问题:

重组开销大,当列表数据发生变更时,LazyColumn需要重新组合受影响的项目。大规模列表意味着更多项目需要重组,从而带来较大的CPU和内存开销。

内存抖动问题,由于Kotlin和Compose都是基于JVM的,因此也会遇到经典的GC和内存抖动问题。大数据列表增加了这方面的风险。

内存抖动(Memory Churn)指的是在运行应用程序时,内存的使用情况存在剧烈的波动,出现内存的急剧分配和释放的情况。这种

绘制效率低,由于Compose需要在低层次API上反复执行测量和布局操作,这会影响最终渲染速度。RecyclerView的ViewHolder模型在此有一定优势。

项目复杂度影响LazyColumn可复用性有限,对于需要复杂状态跟踪的列表项,组合项本身就可能成为性能瓶颈。

针对这些问题,Compose和社区提出了一些优化方案和建议。

一、优化方案 使用Stable声明,利用kotlinx.collections.immutable包提供的List、Map等稳定集合,配合Compose 1.2引入的@Stable注解,可以避免意外的重组发生。 @Composable fun MyItem(@Stable items: List) { // ... }

状态提升,复杂列表项内部状态跟踪也可能带来性能问题。因此建议将状态提升到更高级别进行集中管理,以避免底层地的重组。

进阶的虚拟化技术Compose 1.3引入的LaxyLayoutPolicy API,可以为LazyColumn、LazyRow等定制精细化的虚拟化策略,充分发挥虚拟化优势。

fun pageVirtualizationPolicy(pageSize: Int) = LazyLayoutPolicy { it.policyFor(pageSize = pageSize) }

惰性计算与缓存,对于计算代价昂贵的数据转换或UI组件,可采用延迟加载和缓存等策略,避免重复计算。

限制重组范围Compose提供的DisposableEffect、SideEffects等API,可用于限制重组的影响范围,避免不必要的重组。

合理使用Animations,合理使用Jetpack Compose动画API,可以保证动画流畅性,且不会对其他UI区域造成影响。

7.选择性使用ViewsInCompose,对于无法用Compose实现的特殊UI需求(如自定义View),可以考虑将其包装进AndroidView,并于Compose集成使用。

二、LazyColumn与RecyclerView对比

与RecyclerView的性能对比,经过多轮的性能测试和数据采集,总结出LazyColumn和RecyclerView在大数据列表场景下的一些性能差异:

内存占用:LazyColumn通常比等效的RecyclerView实现占用更多内存,主要是由于Compose本身的内存模型所致,不过差距在合理的范围内。 CPU开销:LazyColumn渲染时的CPU使用率略高于等效的ViewHolder模型。这是由Compose底层的计算和跟踪机制造成的。 第一次渲染耗时:LazyColumn第一次渲染列表时的耗时较长,因为需要组合大量项目。而ViewHolder则只需找到对应的View对象并设置数据。 后续滚动流畅度:滚动过程中,两者的表现接近,都能保持较好的流畅度。不过列表项越复杂,LazyColumn的优势就越小。 视图层级和测量布局:在复杂布局场景下,ViewHolder的扁平视图层级结构可以带来一定优势,避免了内部的测量和布局开销。 定制灵活性:相比ViewHolder的高度自由,Compose目前在定制复杂列表视图仍有些局限性,需要谨慎权衡。 开发体验:在开发体验层,Compose更易于编写、维护和测试,具有响应式编程和声明式UI描述等优势。ViewHolder则需要编写更多样板代码。

总的来说,LazyColumn在性能方面并不比RecyclerView差,而且还有很大的优化空间。实际开发中,我们需要结合具体场景权衡利弊,对于简单列表来说,LazyColumn是更好的选择,但复杂场景下ViewHolder的优势仍然存在。

三、优化Example

下面我将基于最新的Compose 1.6.3版本和Material 3,提供了一个LazyColumn性能优化示例。该示例将包含以下几个部分:

使用不可变集合 使用key避免重复渲染 状态提升 惰性计算和缓存 限制重组范围 合理使用动画 选择性使用ViewsInCompose package com.example.jetnotes.ui.screens import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.view.View import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel fun MutableList.toggle(element: T) = if (contains(element)) remove(element) else add(element) data class MyItem(val id: Int, val name: String) data class MyItemDetails(val id: Int, val details: String) class MyViewModel: ViewModel() { private val _expandedIds = mutableStateListOf() val expandedIds: List = _expandedIds private val listeners = mutableListOf Unit>() fun toggleExpanded(id: Int) { _expandedIds.toggle(id) notifyListeners(id) } fun updateExpandedIds(id: Int) = _expandedIds.toggle(id) fun addListener(listener: (Int) -> Unit) = listeners.add(listener) fun removeListener(listener: (Int) -> Unit) = listeners.remove(listener) private fun notifyListeners(id: Int) { listeners.forEach { it(id) } } } @Composable fun OptimizedLazyColumnDemo( items: List = List(100) { MyItem(it, "Item $it") }, viewModel: MyViewModel = viewModel() ) { val stableItems = items.toList() // 1.使用不可变集合 LazyColumn( content = { itemsIndexed(stableItems) { _, item -> key(item.id) { // 2.使用key避免重复渲染 val isExpanded = viewModel.expandedIds.contains(item.id) MyItemView( item = item, isExpanded = isExpanded, onExpandedChange = { viewModel.toggleExpanded(item.id) } ) } } } ) } @Composable fun MyItemView( item: MyItem, isExpanded: Boolean, onExpandedChange: (Int) -> Unit ) { val transition = updateTransition(targetState = isExpanded, label = "MyItemViewTransition") val height by transition.animateDp(label = "heightTransition") { if (it) 200.dp else 50.dp // 6.合理使用动画 } Card( modifier = Modifier .fillMaxWidth() .padding(8.dp), onClick = { onExpandedChange(item.id) } ) { Column( modifier = Modifier .padding(16.dp) .height(height) ) { Text(text = item.name, style = MaterialTheme.typography.titleLarge, color = Color.Red) if (isExpanded) { Spacer(modifier = Modifier.height(8.dp)) val details = remember(item.id) { calculateDetails(item.id) } // 4.惰性计算与缓存 DisposableEffect(key1 = item.id, effect = { // 5. 限制重组范围 // 执行一些副作用操作 onDispose { } }) AndroidView( factory = { context -> MyCustomView(context) // 7.选择性使用ViewsInCompose }, update = { view -> view.setData(details) } ) } } } } fun calculateDetails(id: Int): MyItemDetails { // 执行一些昂贵的计算操作 return MyItemDetails(id, "Details for item @id") } class MyCustomView(context: Context): View(context) { private var data: MyItemDetails? = null private val paint = Paint() init { paint.textSize = 40f } fun setData(details: MyItemDetails) { data = details invalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) data?.let { canvas.drawText("ID: ${it.id}", 0f, 50f, paint) canvas.drawText("Details: ${it.details}", 0f, 100f, paint) } } } @Preview(showBackground = true) @Composable fun OptimizedLazyColumnDemoPreview() { MaterialTheme { OptimizedLazyColumnDemo() } }

以下是对各部分代码的详细解释:

使用不可变集合

我们使用items.toList()将传入的List转换为不可变列表。这样可以确保在后续重组过程中,列表本身不会被意外修改,从而避免不必要的重组。

使用Key避免重复渲染

在LazyColumn的item作用域内,我们使用key(item.id)为每个项目指定了唯一的key。这可以避免相同数据的项目被记重复渲染,从而减少不必要的计算开销。

状态提升

我们将列表项的展开/折叠状态提升到MyViewModel中进行集中管理。这样可以避免在每个列表内部都维护自己的状态,从而减少层次的重组。

惰性计算和缓存

对于计算代价昂贵的calculateDetail操作,我们使用remember函数将结果缓存起来。当相同的item.id再次出现时,就可以直接从缓存中获取结果,避免重复计算。

限制重组范围

通过LaunchedEffect和key1参数,我们限制了副作用操作的重组范围。只有当item.id发生变化时,相关的骨作用才会被重新执行,从而避免了不必要的重组。

合理使用动画

使用updateTransition和animateHeight实现了一个简单的展开/折叠动画效果。这种方式可以确保动画流畅,且不会影响其他UI区域的性能。

选择性使用ViewsInCompose

对于无法用Compose实现的自定义View,我们使用AndroidView将其包装进来,并与Compose集成使用。这种混合使用方式可以发挥两者的优势,提高灵活性。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有